<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>敏感词匹配</title>
    <style>
      * {
        margin: 0;
        padding: 0;
      }
      html {
        font: 16px;
      }
      body {
        font: 14px/1.5 monospace;
      }
      main {
        display: grid;
        grid-template-columns: repeat(2, 1fr);
        grid-template-rows: repeat(2, 1fr);
        gap: 2rem;
        width: 60rem;
        margin: 0 auto;
        padding: 2rem;
      }

      fieldset {
        padding: 1rem;
        border: 1px solid #ccc;
      }
      fieldset:last-child {
        grid-column: 1 / 3;
      }
      legend {
        padding: 0 0.5rem;
      }

      textarea {
        box-sizing: border-box;
        width: 100%;
        min-height: 300px;
        padding: 1em;
        border-color: #ddd;
      }
      article {
        /* text-indent: 2em; */
      }

      b {
        color: #f00;
      }
      b::before,
      b::after {
        content: '[';
        font-size: 0.8em;
        color: #ccc;
      }
      b::after {
        content: ']';
      }
    </style>
  </head>
  <body>
    <main>
      <fieldset>
        <legend>敏感词</legend>
        <textarea
          id="keywords"
          placeholder="请输入敏感词，每行一个。格式：最低价 不能使用最低价"
          spellcheck="false"
        ></textarea>
      </fieldset>
      <fieldset>
        <legend>文章</legend>
        <textarea
          id="article"
          placeholder="请输入文章内容"
          spellcheck="false"
        ></textarea>
      </fieldset>
      <fieldset>
        <legend>匹配结果</legend>
        <article id="output"></article>
      </fieldset>
    </main>

    <script>
      class Storage {
        constructor(name) {
          this.name = name;
        }
        setValue(value) {
          localStorage.setItem(this.name, JSON.stringify(value));
        }
        getValue() {
          return JSON.parse(localStorage.getItem(this.name));
        }
      }

      var storageFactory = {
        create(name) {
          return new Storage(name);
        },
      };

      var keywordsStorage = storageFactory.create('keywords');
      var $keywords = document.querySelector('#keywords');
      var $article = document.querySelector('#article');

      var timer = null;
      function calcAndRender() {
        clearTimeout(timer);
        timer = setTimeout(() => {
          let keywordMap = {};
          $keywords.value.split(/\n/).forEach((item) => {
            let [key, message] = item.trim().split(/[\t\s]+/);
            if (!key) return;
            keywordMap[key] = message || '';
          });

          let article = $article.value;

          var markList = [];
          var result = article;
          Object.entries(keywordMap).forEach(([key, message], keyIndex) => {
            let lastIndex = 0;
            let len = key.length;
            while (1) {
              let resultLen = result.length;
              let index = result.indexOf(key, lastIndex);
              if (index === -1) break;

              let fromIndex = index;
              let toIndex = index + len;

              markList.push({
                fromIndex: index,
                toIndex: index + len,
                message,
              });
              lastIndex = index + 1;
            }
          });
          renderArticle(article, markList);
        }, 300);
      }

      function renderArticle(article, markList) {
        // debugger;
        if (!article.length || !markList.length) {
          html = article;
        } else {
          var list = markList.slice();
          list.sort((a, b) => {
            return a.fromIndex - b.fromIndex || b.toIndex - a.toIndex;
          });

          var fromMarks = {};
          var toMarks = {};
          list.forEach(({ fromIndex, toIndex, message }) => {
            if (!fromMarks[fromIndex]) {
              fromMarks[fromIndex] = [];
            }
            fromMarks[fromIndex].push(message);

            if (!toMarks[toIndex]) {
              toMarks[toIndex] = [];
            }
            toMarks[toIndex].push(message);
          });

          html = article.replace(/[\s\S]/g, function (match, offset, str) {
            return `${(toMarks[offset] || [])
              .map((msg) => `</b>`)
              .join(
                ''
              )}${(fromMarks[offset] || []).map((msg) => `<b title="${escapeMsg(msg)}">`).join('')}${match}`;
          });
        }
        document.querySelector('#output').innerHTML = `<p>${html.replace(
          /\n/g,
          '</p></p>'
        )}</p>`;
      }

      function escapeMsg(msg) {
        return msg.replace(/"/g, '&#34');
      }

      $keywords.value = keywordsStorage.getValue() || '';
      $keywords.addEventListener(`input`, function (e) {
        var text = e.target.value;
        keywordsStorage.setValue(text);
        calcAndRender();
      });

      $article.addEventListener(`input`, function (e) {
        calcAndRender();
      });
    </script>
  </body>
</html>
